
package config

import (
	"context"
	"fmt"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"go.etcd.io/etcd/client/pkg/v3/transport"
	"go.etcd.io/etcd/client/pkg/v3/types"
	"go.etcd.io/etcd/pkg/v3/netutil"
	"go.etcd.io/etcd/server/v3/storage/datadir"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"

	bolt "go.etcd.io/bbolt"
	"go.uber.org/zap"
)

// ServerConfig保存从命令行或发现中获取的etcd配置。
type ServerConfig struct {
	Name           string
	DiscoveryURL   string
	DiscoveryProxy string
	ClientURLs     types.URLs
	PeerURLs       types.URLs
	DataDir        string
	// 专用WADIR配置将使etcd将WAL写入WADIR 
	// 而不是dataDir/member/WAL。
	DedicatedWALDir string

	SnapshotCount uint64

	// SnapshotCatchUpEntries是慢速跟随者压缩raft存储条目后追赶的条目数
	// 。
	// 我们希望跟随者与引导者之间有毫秒级的延迟。
	// 最大吞吐量约为10K。保持5公里的参赛成绩足以帮助
	// 追随者迎头赶上。
	// 警告：仅对测试更改此选项。始终使用“DefaultSnapshotCatchUpEntries”
	SnapshotCatchUpEntries uint64

	MaxSnapFiles uint
	MaxWALFiles  uint

	// BackendBatchInterval是提交后端事务之前的最长时间。
	BackendBatchInterval time.Duration
	// BackendBatchLimit是提交后端事务之前的最大操作数。
	BackendBatchLimit int

	// BackendFreelistType是后端boltdb freelist的类型。
	BackendFreelistType bolt.FreelistType

	InitialPeerURLsMap  types.URLsMap
	InitialClusterToken string
	NewCluster          bool
	PeerTLSInfo         transport.TLSInfo

	CORS map[string]struct{}

	// 主机白名单列出客户端请求中可接受的主机名。
	// 如果服务器不安全（没有TLS），则服务器只接受主机头值在此白名单中的请求
	// 。
	HostWhitelist map[string]struct{}

	TickMs        uint
	ElectionTicks int

	// 初始选举滴答前进为真，则本地成员快进
	// 选举滴答声加快“初始”领导人选举触发。本
	// 有利于更大的选举周期。例如，跨
	// 数据中心部署可能需要更长的10秒选择超时。
	// 如果为true，则本地节点不需要等待10秒。相反，在领导人选举前，
	// 将其选举时间缩短到8秒，只剩下2秒。
	// 
	// 主要假设是：
	// -集群没有活跃的领导者，因此前进的滴答声可以加快
	// 领导者选举，或者
	// -集群已经有了一个固定的领导者，而重新加入追随者
	// 很可能会在提前点击
	// 后和选举暂停前收到领导者的心跳。
	// 
	// 但是，当从领导者到重新加入追随者的网络拥塞时，
	// 并且追随者在左选举中没有接收到领导者心跳
	// 滴答声，必须发生破坏性选举，从而影响集群
	// 可用性。
	// 
	// 禁用此选项将减慢跨
	// 数据中心部署的初始引导过程。通过配置
	// /--initial election tick advance，以缓慢的初始引导为代价，做出您自己的权衡。
	// 
	// 如果是单个节点，则无论如何都会前进。
	// 
	// 请参阅https:
	InitialElectionTickAdvance bool

	BootstrapTimeout time.Duration

	AutoCompactionRetention time.Duration
	AutoCompactionMode      string
	CompactionBatchLimit    int
	CompactionSleepInterval time.Duration
	QuotaBackendBytes       int64
	MaxTxnOps               uint

	// MaxRequestBytes是通过raft发送的最大请求大小。
	MaxRequestBytes uint

	WarningApplyDuration        time.Duration
	WarningUnaryRequestDuration time.Duration

	StrictReconfigCheck bool

	// 当客户端CA对证书进行签名时，ClientCertAuthEnabled为true。
	ClientCertAuthEnabled bool

	AuthToken  string
	BcryptCost uint
	TokenTTL   uint

	// InitialCorruptCheck为true，用于检查启动时的数据损坏
	// 在服务任何对等/客户端通信之前。
	InitialCorruptCheck bool
	CorruptCheckTime    time.Duration

	// PreVote为真，用于启用Raft预投票。
	PreVote bool

	// SocketOpts是传递给侦听器配置的套接字选项。
	SocketOpts transport.SocketOpts

	// 记录器记录服务器端操作。
	Logger *zap.Logger

	ForceNewCluster bool

	// EnableLeaseCheckpoint允许leader向其他成员发送定期检查点，以防止在leader更改时重置剩余TTL。
	EnableLeaseCheckpoint bool
	// 最小切点间隔时间。Duration是租约检查点之间的等待时间。
	LeaseCheckpointInterval time.Duration
	// LeaseCheckpointPersist启用持久保留TTL，以防止长期租约的无限期自动续订。始终在v3中启用。6.应用于确保从v3顺利升级。启用此功能的5个群集。
	LeaseCheckpointPersist bool

	EnableGRPCGateway bool

	// ExperimentableDistributedTracing使用OpenTelemetry协议启用分布式跟踪。
	ExperimentalEnableDistributedTracing bool
	// 实验记录是OpenTelemetry gRPC拦截器的选项。
	ExperimentalTracerOptions []otelgrpc.Option

	WatchProgressNotifyInterval time.Duration

	// UnsafeNoFsync禁用fsync的所有使用。
	// 此设置不安全，会导致数据丢失。
	UnsafeNoFsync bool `json:"unsafe-no-fsync"`

	DowngradeCheckTime time.Duration

	// ExperimentalMemoryMlock启用etcd拥有的内存页的MLock。
	// 该设置改善了环境中的etcd尾部延迟：
	// -内存压力可能导致将页面交换到磁盘
	// -磁盘延迟可能不稳定
	// 当前所有etcd内存都被锁定，但在将来，该标志可被细化为仅在bbolt使用区域锁定。
	ExperimentalMemoryMlock bool `json:"experimental-memory-mlock"`

	// ExperimentalTxnModeWriteWithSharedBuffer使写事务在其只读检查操作中使用
	// 共享缓冲区。ABCFDG
	ExperimentalTxnModeWriteWithSharedBuffer bool `json:"experimental-txn-mode-write-with-shared-buffer"`

	ExperimentalBootstrapDefragThresholdMegabytes uint `json:"experimental-bootstrap-defrag-threshold-megabytes"`

	// 实验型学习者对集群成员中可能存在的学习者成员数量设置限制。
	ExperimentalMaxLearners int `json:"experimental-max-learners"`

	// V2Deprecation定义了v2store折旧过程的一个阶段。
	V2Deprecation V2DeprecationEnum `json:"v2-deprecation"`
}

// VerifyBootstrap健全性检查引导案例的初始配置
// 并返回一个错误，指出不应该发生的事情。
// ServerConfig.VerifyBootstrap（）方法，其中主要检测当前配置中是否包含当前
// 节点，检测当前节点提供的 URL 与 initial-advertise-peer-urls 配置项是否相同，检测集群配置中
// 是否存在重复的 URL 地址， 等等，
func (c *ServerConfig) VerifyBootstrap() error {
	if err := c.hasLocalMember(); err != nil {
		return err
	}
	if err := c.advertiseMatchesCluster(); err != nil {
		return err
	}
	if CheckDuplicateURL(c.InitialPeerURLsMap) {
		return fmt.Errorf("initial cluster %s has duplicate url", c.InitialPeerURLsMap)
	}
	if c.InitialPeerURLsMap.String() == "" && c.DiscoveryURL == "" {
		return fmt.Errorf("initial cluster unset and no discovery URL found")
	}
	return nil
}

// VerifyJoinExisting sanity检查加入现有集群的初始配置
// case并返回一个错误，指出不应该发生的事情。
func (c *ServerConfig) VerifyJoinExisting() error {
	// 成员在启动前已宣布其指向集群的对等URL；无需
	// 重新设置配置。
	if err := c.hasLocalMember(); err != nil {
		return err
	}
	if CheckDuplicateURL(c.InitialPeerURLsMap) {
		return fmt.Errorf("initial cluster %s has duplicate url", c.InitialPeerURLsMap)
	}
	if c.DiscoveryURL != "" {
		return fmt.Errorf("discovery URL should not be set when joining existing initial cluster")
	}
	return nil
}

// hasLocalMember检查集群是否至少包含本地服务器。
func (c *ServerConfig) hasLocalMember() error {
	if urls := c.InitialPeerURLsMap[c.Name]; urls == nil {
		return fmt.Errorf("couldn't find local name %q in the initial cluster configuration", c.Name)
	}
	return nil
}

// advertiseMatchesCluster确认对等URL与集群对等列表中的URL匹配。
func (c *ServerConfig) advertiseMatchesCluster() error {
	urls, apurls := c.InitialPeerURLsMap[c.Name], c.PeerURLs.StringSlice()
	urls.Sort()
	sort.Strings(apurls)
	ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
	defer cancel()
	ok, err := netutil.URLStringsEqual(ctx, c.Logger, apurls, urls.StringSlice())
	if ok {
		return nil
	}

	initMap, apMap := make(map[string]struct{}), make(map[string]struct{})
	for _, url := range c.PeerURLs {
		apMap[url.String()] = struct{}{}
	}
	for _, url := range c.InitialPeerURLsMap[c.Name] {
		initMap[url.String()] = struct{}{}
	}

	missing := []string{}
	for url := range initMap {
		if _, ok := apMap[url]; !ok {
			missing = append(missing, url)
		}
	}
	if len(missing) > 0 {
		for i := range missing {
			missing[i] = c.Name + "=" + missing[i]
		}
		mstr := strings.Join(missing, ",")
		apStr := strings.Join(apurls, ",")
		return fmt.Errorf("--initial-cluster has %s but missing from --initial-advertise-peer-urls=%s (%v)", mstr, apStr, err)
	}

	for url := range apMap {
		if _, ok := initMap[url]; !ok {
			missing = append(missing, url)
		}
	}
	if len(missing) > 0 {
		mstr := strings.Join(missing, ",")
		umap := types.URLsMap(map[string]types.URLs{c.Name: c.PeerURLs})
		return fmt.Errorf("--initial-advertise-peer-urls has %s but missing from --initial-cluster=%s", mstr, umap.String())
	}

	// 从“-初始播发对等URL”和“-初始群集”解析的URL不匹配或失败
	apStr := strings.Join(apurls, ",")
	umap := types.URLsMap(map[string]types.URLs{c.Name: c.PeerURLs})
	return fmt.Errorf("failed to resolve %s to match --initial-cluster=%s (%v)", apStr, umap.String(), err)
}

func (c *ServerConfig) MemberDir() string { return datadir.ToMemberDir(c.DataDir) }

func (c *ServerConfig) WALDir() string {
	if c.DedicatedWALDir != "" {
		return c.DedicatedWALDir
	}
	return datadir.ToWalDir(c.DataDir)
}

func (c *ServerConfig) SnapDir() string { return filepath.Join(c.MemberDir(), "snap") }

func (c *ServerConfig) ShouldDiscover() bool { return c.DiscoveryURL != "" }

// ReqTimeout返回请求完成超时。
func (c *ServerConfig) ReqTimeout() time.Duration {
	// 5s用于队列等待、计算和磁盘IO延迟
	// +2*选举超时用于可能的领导人选举
	return 5*time.Second + 2*time.Duration(c.ElectionTicks*int(c.TickMs))*time.Millisecond
}

func (c *ServerConfig) ElectionTimeout() time.Duration {
	return time.Duration(c.ElectionTicks*int(c.TickMs)) * time.Millisecond
}

func (c *ServerConfig) PeerDialTimeout() time.Duration {
	// /1s用于队列等待和选举超时
	return time.Second + time.Duration(c.ElectionTicks*int(c.TickMs))*time.Millisecond
}

func CheckDuplicateURL(urlsmap types.URLsMap) bool {
	um := make(map[string]bool)
	for _, urls := range urlsmap {
		for _, url := range urls {
			u := url.String()
			if um[u] {
				return true
			}
			um[u] = true
		}
	}
	return false
}

func (c *ServerConfig) BootstrapTimeoutEffective() time.Duration {
	if c.BootstrapTimeout != 0 {
		return c.BootstrapTimeout
	}
	return time.Second
}

func (c *ServerConfig) BackendPath() string { return datadir.ToBackendFileName(c.DataDir) }
